/*
Java base64-encoding FilterInputStream
Copyright (c) 1999 by Michael N. Lipp

This program is free software.

You may redistribute it and/or modify it under the terms of the GNU
General Public License as published by the Free Software Foundation.
Version 2 of the license should be included with this distribution in
the file COPYING.GPL. If the license is not included	with this 
distribution, you may find a copy at the FSF web site 
at 'www.gnu.org' or 'www.fsf.org', or you may write to the
Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139 USA.

THIS SOFTWARE IS PROVIDED AS-IS WITHOUT WARRANTY OF ANY KIND,
NOT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY. THE AUTHOR
OF THIS SOFTWARE, ASSUMES _NO_ RESPONSIBILITY FOR ANY
CONSEQUENCE RESULTING FROM THE USE, MODIFICATION, OR
REDISTRIBUTION OF THIS SOFTWARE. 
*/
package gnu.inet.mime.base64;

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

/** decode a raw stream encoded in base 64 on the fly.
* The base 64 algorithm is defined in RFC1521.
*
* @author Michael N. Lipp
*/
public class Base64InputStream extends FilterInputStream
{

	private int surplus;
	private int decoded;
	private int lastRead = 0;

	/**
	 * The constructor for the decoding input stream.
	 *
	 * @param in the underlying input stream
	 */
	public Base64InputStream(InputStream in)
	{
		super(in);
		decoded = 0;
	}

	/**
	 * This method always return 0, as we really can't be sure about the
	 * number of decoded bytes available without doing the decoding. Imaging
	 * an underlying input stream from which several whitespace characters
	 * can still be read. Thus calling available() on the underlying stream
	 * returns a number greater than zero. But whitespace decodes to nothing,
	 * so we can't derive a reliable number for available from he underlying
	 * stream.
	 */
	public int available() throws IOException
	{
		return 0;
	}

	/**
	 * Always returns false, since marking is not supported.
	 */
	public boolean markSupported()
	{
		return false;
	}

	/**
	 * Reads a single byte. Returns -1 if the input stream is exhausted.
	 */
	public int read() throws IOException
	{
		byte b[] = new byte[1];
		int res = read(b);
		if (res < 0)
			return res;
		return (b[0] & 0xff);
	}

	/**
	 * Reads len bytes from the input stream, saving them in the array
	 * data beginning at index off. 
	 * Returns -1 if the input stream is exhausted.
	 *
	 * @param data the array with bytes having been read.
	 * @param off the position of the first byte read within the array.
	 * @param len the number of bytes to be read.
	 */
	public int read(byte[] data, int off, int len) throws IOException
	{
		int pos = off;
		// EOF is EOF is EOF...
		if (lastRead < 0)
			return -1;

		// get from buf while not on boundary
		while (len > 0 && decoded > 0 && lastRead >= 0)
		{
			int b = decodeByte();
			if (b < 0)
			{
				break;
			}
			data[pos++] = (byte)b;
			len -= 1;
		}
		// shortcut while complete groups are requested
		while (len >= 3 && lastRead >= 0)
		{
			int grp0 = 0, grp1 = 0, grp2 = 0, grp3 = 0;
			int gotCnt = 0;
			while (gotCnt < 4)
			{
				lastRead = in.read();
				if (lastRead < 0)
					break;
				if (lastRead == '=')
				{
					// end of encoded data
					if (gotCnt == 2)
					{
						// a proper base64 sequence has a multiple of
						// 4 characters, so another '=' should follow. Align.
						in.read();
					}
					lastRead = -1;
					break;
				}
				int grp = codedBits((char)lastRead);
				if (grp >= 0)
				{
					switch (gotCnt++)
					{
						case 0:
							grp0 = grp;
							break;
						case 1:
							grp1 = grp;
							break;
						case 2:
							grp2 = grp;
							break;
						case 3:
							grp3 = grp;
							break;
					}
				}
			}
			switch (gotCnt)
			{
				case 4:
					data[pos + 2] = (byte)((grp2 << 6) | (grp3));
				case 3:
					data[pos + 1] = (byte)((grp1 << 4) | (grp2 >>> 2));
				case 2:
					data[pos] = (byte)((grp0 << 2) | (grp1 >>> 4));
					pos += (gotCnt - 1);
					len -= (gotCnt - 1);
			}
		}
		// get rest of requested input
		while (len > 0 && lastRead >= 0)
		{
			int b = decodeByte();
			if (b < 0)
			{
				break;
			}
			data[pos++] = (byte)b;
			len -= 1;
		}
		if (len > 0 && pos == off)
			return -1;
		return pos - off;
	}

	private int decodeByte() throws IOException
	{
		int b;
		switch (decoded)
		{
			case 0:
				do
				{
					lastRead = in.read();
					if (lastRead < 0 || lastRead == '=')
					{
						lastRead = -1;
						return -1;
					}
				}
				while ((b = codedBits((char)lastRead)) < 0);
				b = (b << 2);
				do
				{
					lastRead = in.read();
					if (lastRead < 0 || lastRead == '=')
					{
						lastRead = -1;
						return -1;
					}
				}
				while ((surplus = codedBits((char)lastRead)) < 0);
				b = b | (surplus >>> 4);
				surplus = (surplus & 0xf);
				decoded = 1;
				return b;

			case 1:
				b = (surplus << 4);
				do
				{
					lastRead = in.read();
					if (lastRead < 0 || lastRead == '=')
					{
						if (lastRead == '=')
						{
							// a proper base64 sequence has a multiple of
							// 4 characters, so another '=' should follow. Align.
							in.read();
						}
						lastRead = -1;
						return -1;
					}
				}
				while ((surplus = codedBits((char)lastRead)) < 0);
				b = b | (surplus >>> 2);
				surplus = (surplus & 0x3);
				decoded = 2;
				return b;

			case 2:
				b = (surplus << 6);
				do
				{
					lastRead = in.read();
					if (lastRead < 0 || lastRead == '=')
					{
						lastRead = -1;
						return -1;
					}
				}
				while ((surplus = codedBits((char)lastRead)) < 0);
				b = b | surplus;
				decoded = 0;
				return b;
		}
		// never realastReaded, but compiler can't know and complains
		return -1;
	}

	private int codedBits(char c)
	{
		if ('A' <= c && c <= 'Z')
			return c - 'A';
		if ('a' <= c && c <= 'z')
			return c - 'a' + 26;
		if ('0' <= c && c <= '9')
			return c - '0' + 52;
		if (c == '+')
			return 62;
		if (c == '/')
			return 63;
		return -1;
	}

}
